﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security;
using System.Security.Cryptography;
using System.Security.Permissions;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;

namespace ExchangeHog.EHCore
{
	[XmlRoot]
	public class EHCoreConfig
	{
		public List<ConnectInfo> sources;
		public ConnectInfo target;

		public EHCoreConfig() {}
	}

	public class ConnectInfo
	{
		public enum ConnectType { POP3, IMAP };
		public ConnectType type; 
		public String server;
		public Int16 port;
		public bool useSSL;
		public String userid;
		public String encryptedpassword;
		public int nDaysPurgeAfter;

		public String id { get { return server + "@" + userid + ":" + port; } }

		public ConnectInfo() { }

		internal ConnectInfo(ConnectType type, String server, Int16 port, bool useSSL, String userid, String encryptedpassword)
		{
			this.type = type;
			this.server = server;
			this.port = port;
			this.useSSL = useSSL;
			this.userid = userid;
			this.encryptedpassword = encryptedpassword;
			nDaysPurgeAfter = -1;
		}

        public ConnectInfo(String[] args) // protocol, server, port, useSSL, userid
        {
            if (args.Length < 4)
                throw new ArgumentException("Invalid number of arguments: expected type, server, port and useSSL");
			this.type = args[0].ToLower().CompareTo("pop3") == 0 ? ConnectType.POP3 : ConnectType.IMAP;
            this.server = args[1];
            try
            {
                this.port = Int16.Parse(args[2]);
                this.useSSL = bool.Parse(args[3]);
            }
            catch (FormatException fe)
            {
                throw new ArgumentException("Invalid argument: " + fe);
            }
            this.userid = args[4];
            this.encryptedpassword = "";
			nDaysPurgeAfter = -1;
        }
	}

	public class EHCore
	{
		String EH_CONTAINER_NAME = "ExchangeHog";

		public EHCoreConfig config = new EHCoreConfig();

		RSACryptoServiceProvider rsa;

		public EHCore()
		{
			CspParameters cp = new CspParameters();
			cp.KeyContainerName = EH_CONTAINER_NAME;
 			cp.Flags = CspProviderFlags.UseMachineKeyStore;
			rsa = new RSACryptoServiceProvider(cp);
		}

		String encryptPassword(String password)
		{
			byte[] plainbytes = System.Text.Encoding.UTF8.GetBytes(password);
			byte[] cipherbytes = rsa.Encrypt(plainbytes, false);
			return Convert.ToBase64String(cipherbytes);
		}

		String decryptPassword(String encryptedpassword)
		{
			byte[] getpassword = Convert.FromBase64String(encryptedpassword);
			byte[] plain = rsa.Decrypt(getpassword, false);
			return System.Text.Encoding.UTF8.GetString(plain);
		}

		[XmlRoot]
		public class ProcessedItems
		{
			[XmlIgnore]
			Dictionary<string, DateTime> processeditems;
			[XmlIgnore]
			public Dictionary<string, DateTime> TheItems
			{
				get { if (processeditems == null) processeditems = (list != null) ? ToDictionary() : new Dictionary<string, DateTime>(); return processeditems; }
				set { processeditems = value; }
			}

			public class MessageIDAndTimeStamp
			{
				public String messageID;
				public DateTime timestamp;
			}
			private List<MessageIDAndTimeStamp> list;

			[XmlElement]
			public List<MessageIDAndTimeStamp> List
			{
				get
				{
					if (list == null)
						list = new List<MessageIDAndTimeStamp>();

					if (processeditems != null)
					{
						list.Clear();
						foreach (var pair in processeditems)
							list.Add(new MessageIDAndTimeStamp { messageID = pair.Key, timestamp = pair.Value });
					}
					return list;
				}
			}

			public Dictionary<string, DateTime> ToDictionary()
			{
				return List.ToDictionary(key => key.messageID, value => value.timestamp);
			}

			public ProcessedItems() { }
		}

		public Dictionary<String, ProcessedItems> mapClientToProcessedItems = new Dictionary<string, ProcessedItems>();

		private String getEHProcessItemsPath()
		{
			return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "eh");
		}

		private String getProcessItemsFilename(ConnectInfo ci)
		{
			StringBuilder sb = new StringBuilder();
			foreach (Char ch in ci.id)
				sb.Append(Char.IsLetterOrDigit(ch)? ch: '_');
			sb.Append(".xml");
			return sb.ToString();
		}

		private void loadProcessedItems()
		{
			String path = getEHProcessItemsPath();
			if (Directory.Exists(path))
			{
				foreach (ConnectInfo pop3 in config.sources)
				{
					String filename = Path.Combine(path, getProcessItemsFilename(pop3));
					if (File.Exists(filename))
						try
						{
							XmlSerializer mySerializer = new XmlSerializer(typeof(ProcessedItems), new Type[] { typeof(HashSet<string>) });

							using (FileStream s = File.Open(filename, FileMode.Open))
							{
								ProcessedItems pi = (ProcessedItems)mySerializer.Deserialize(s);
								mapClientToProcessedItems.Add(pop3.id, pi);
							}
						}
						catch(InvalidOperationException e)
						{
							Console.Out.WriteLine("Failed to read xml from {0}: {1}", filename, e);
						}
				}
			}
		}

		private void saveProcessedItems(ConnectInfo ci, ProcessedItems pi)
		{
			String path = getEHProcessItemsPath();
			Directory.CreateDirectory(path);
			String filename = Path.Combine(path, getProcessItemsFilename(ci));

			XmlSerializer mySerializer = new XmlSerializer(typeof(ProcessedItems), new Type[] { typeof(HashSet<string>) });

			using (StreamWriter myWriter = new StreamWriter(filename))
			{
				mySerializer.Serialize(myWriter, pi);
			}
		}

		String filenameConfiguration = null;
		public void loadConfiguration(String filename)
		{
			filenameConfiguration = filename;
			XmlSerializer mySerializer = new XmlSerializer(typeof(EHCoreConfig));

			FileStream s = File.Open(filename, FileMode.Open);
			config = (EHCoreConfig)mySerializer.Deserialize(s);
			s.Close();
		}

		public void createTestConfiguration(String filename)
		{
			config.sources = new ConnectInfo[]
				{
					new ConnectInfo(ConnectInfo.ConnectType.POP3, "mail.co", 110, true, "userid", ""),
					new ConnectInfo(ConnectInfo.ConnectType.IMAP, "anothermail.co", 110, true, "anotheruserid", "")
				}.ToList<ConnectInfo>();
			config.target = new ConnectInfo(ConnectInfo.ConnectType.POP3, "targetimap", 143, true, "myuserid", "");

			saveConfiguration(filename);
		}

		public void saveConfiguration(String filename)
		{
			XmlSerializer mySerializer = new XmlSerializer(typeof(EHCoreConfig));

			StreamWriter myWriter = new StreamWriter(filename);
			mySerializer.Serialize(myWriter, config);
			myWriter.Close();
		}

		SecureString getPassword(ConnectInfo ci)
		{
			String encryptedpwd = ci.encryptedpassword;
			SecureString result;
			if (encryptedpwd.Length > 0)
				result = Util.StringToSecureString(decryptPassword(encryptedpwd));
			else
			{
				Console.WriteLine("Enter password for {0}:", ci.id);
				StringBuilder sb = new StringBuilder();
				while(true)
				{
					var cki = Console.ReadKey(true);
					if (cki.Key == ConsoleKey.Enter)
						break;
					sb.Append(cki.KeyChar);
				}
				ci.encryptedpassword = encryptPassword(sb.ToString());
				result = Util.StringToSecureString(sb.ToString());
				saveConfiguration(filenameConfiguration);
			}
			return result;
		} // end of getPassword

		public void check()
		{
			IMAPClient imapclient = new IMAPClient(config.target.server, config.target.port, config.target.useSSL);
			try
			{
				imapclient.connect();
				imapclient.login(config.target.userid, getPassword(config.target));
			}
			catch (IMAPException se)
			{
				Console.WriteLine("Failed to connect to target imap server: " + se);
				return;
			}

			Console.WriteLine("Connected to " + config.target.server);

			loadProcessedItems();

			foreach (ConnectInfo ciPOP3 in config.sources)
			{
				ProcessedItems processed = null;
				if (!mapClientToProcessedItems.TryGetValue(ciPOP3.id, out processed))
				{
					processed = new ProcessedItems();
					mapClientToProcessedItems[ciPOP3.id] = processed;
				}

				POP3Client pop3client = new POP3Client(ciPOP3.server, ciPOP3.port, ciPOP3.useSSL);
				Console.Write("Connecting to " + ciPOP3.server + " as " + ciPOP3.userid + "...");
				try
				{
					pop3client.connect();
					pop3client.authenticate(ciPOP3.userid, getPassword(ciPOP3));
					Console.WriteLine(" done.");

					LinkedList<POP3Client.MessageNumberAndUIDL> uidls = pop3client.retreiveUIDLs();
					Console.Write("\t" + uidls.Count + " messages");
					foreach (POP3Client.MessageNumberAndUIDL uidl in uidls)
					{
						DateTime dt;
						if (processed.TheItems.TryGetValue(uidl.uidl, out dt))
							if (ciPOP3.nDaysPurgeAfter >= 0 && DateTime.Now.Subtract(dt).Days > ciPOP3.nDaysPurgeAfter)
							{
								pop3client.removeMessage(uidl.messagenumber);
								Console.Write("-");
							}
							else
								Console.Write(".");
						else
						{
							byte[] bytes = ASCIIEncoding.ASCII.GetBytes(pop3client.retrieve(uidl.messagenumber));
							imapclient.addMessage(bytes);
							processed.TheItems.Add(uidl.uidl, DateTime.Now);
							saveProcessedItems(ciPOP3, processed);
							Console.Write("+");
						}
					}
					Console.WriteLine();

					pop3client.quit();
					pop3client.disconnect();
				}
				catch (POP3Exception se)
				{
					Console.WriteLine("Failed to retrieve messages: " + se);
				}
				catch (IMAPException se)
				{
					Console.WriteLine("Failed to retrieve messages: " + se);
				}
			}

			imapclient.disconnect();
			Console.WriteLine("Done");
		}
	}
}
